DigiKat Map 2: Thematic Structure

What is Croatian Catholic Digital Media About?

Author

DigiKat Project

Published

January 27, 2026

1 Introduction

This document presents Map 2: Thematic Structure of the DigiKat project. The core question driving this analysis is: What is Croatian Catholic digital media about, and who specializes in what?

We use topic modeling to discover the thematic landscape of the corpus and then examine how different actors and platforms specialize in particular topics.

1.1 Methodological Overview

Topic modeling is an unsupervised machine learning technique that discovers latent thematic patterns in large text corpora. Unlike manual content analysis, topic models can process hundreds of thousands of documents and identify recurring themes without predefined categories.

We employ Structural Topic Modeling (STM), an extension of Latent Dirichlet Allocation (LDA) that incorporates document metadata (actor type, platform, date) as covariates. This allows us to examine not only what topics exist but also how topic prevalence varies across different communicators and platforms.

Key concepts:

  • Topic: A probability distribution over words. Topics represent coherent themes (e.g., liturgical practice, social issues, Vatican news)
  • Document-topic distribution (theta): Each document is a mixture of topics. A post might be 60% liturgical and 40% devotional
  • Topic prevalence: The average proportion of the corpus devoted to each topic
  • FREX words: Words that are both frequent AND exclusive to a topic (helps distinguish topics)

Interpretation guidance:

  • High prevalence topics represent dominant themes in the discourse
  • Topics with high FREX distinctiveness are easier to interpret
  • Actor-topic associations reveal thematic specialization
  • Platform-topic patterns show how different media afford different content types
Show code
dta <- readRDS("C:/Users/lsikic/Luka C/HKS/Projekti/Digitalni Kat/SHKM/DigiKat/data/merged_comprehensive.rds") %>%
  filter(SOURCE_TYPE != "tiktok", !is.na(SOURCE_TYPE)) %>%
  filter(DATE >= as.Date("2021-01-01") & DATE <= as.Date("2025-12-31")) %>%
  filter(year >= 2021 & year <= 2025)
setDT(dta)

# Check data loaded correctly
cat("Data loaded:", nrow(dta), "rows\n")
Data loaded: 608879 rows
Show code
if (nrow(dta) == 0) stop("No data after filtering! Check your date filters.")

n_posts <- nrow(dta)
n_with_text <- sum(!is.na(dta$FULL_TEXT) & nchar(dta$FULL_TEXT) > 50, na.rm = TRUE)
Show code
# =============================================================================
# ACTOR CLASSIFICATION FOR MAP 2
# =============================================================================
# This uses the same v4 classification as Map 1 for consistency.
# See Map 1 documentation for full explanation of classification logic.
# =============================================================================

# Manual overrides for high-engagement sources
manual_overrides <- list(
  "Institutional Official" = c(
    "hrvatska katolička mreža", "hrvatska katolicka mreza",
    "informativna katolička agencija", "ika", "hkr", "hkm", "hbk",
    "tiskovni ured hbk", "radio marija"
  ),
  "Independent Media" = c(
    "laudato tv", "laudatotv", "laudato.tv", "laudato.hr", "laudato",
    "bitno.net", "glas koncila", "glaskoncila",
    "nova eva", "nova-eva", "verbum", "totus tuus",
    "novizivot.net", "novi zivot", "novi život"
  ),
  "Charismatic Communities" = c(
    "božja pobjeda", "bozja pobjeda", "muževni budite", "muzevni budite",
    "srce isusovo", "cenacolo", "duhovna obnova", "molitvena snaga",
    "dom molitve slavonski brod", "dom molitve", "molitvena zajednica sv. josipa"
  ),
  "Lay Influencers" = c(
    "katolička obitelj", "katolicka obitelj", "marija majka isusova",
    "božanske molitve", "moćne molitve", "katoličke molitve",
    "pulherissimus", "pod smokvom", "hrana za dušu", "hrana za dusu",
    "добровољци", "miletić marin", "dijete vjere", "vjera",
    "kapljice ljubavi božje", "kršćanstvo", "jutarnja molitva duhu svetom",
    "blago molitve", "biblija krunice molitve"
  ),
  "Diocesan" = c(
    "zagrebačka nadbiskupija", "sisačka biskupija",
    "župa šurkovac", "sveta mati slobode",
    "župa sv. ilije proroka metković", "župa uznesenja bdm",
    "šibenska biskupija", "požeška biskupija"
  ),
  "Youth Organizations" = c(
    "susret hrvatske katoličke mladeži", "shkm požega"
  ),
  "Academic" = c(
    "hrvatsko katoličko sveučilište", "universitas studiorum catholica croatica"
  )
)

secular_media_exclusions <- c(
  "slobodnadalmacija", "vecernji", "index.hr", "jutarnji", "novilist",
  "24sata", "direktno.hr", "nacional", "tportal", "dnevnik.hr", "hrt.hr",
  "n1info", "rtl.hr", "net.hr", "telegram.hr", "story.hr", "forum.hr",
  "glasistre", "dnevno.hr", "prigorski", "glas-slavonije", "croativ",
  "oluja.info", "maxportal", "hkv.hr", "icv.hr", "novosti.hr", "7dnevno",
  "mnovine", "sjever.hr", "dulist.hr", "pozega.eu", "sibenik.in",
  "ferata.hr", "epodravina", "glasgacke", "radio-zlatar", "medjimurski.hr",
  "sbperiskop", "zagorje-international", "pozeski", "novine.hr",
  "dubrovnikinsider", "regionalni", "leportale", "varazdinske-vijesti",
  "radionasice", "brodportal", "ljportal", "dubrovnikportal", "01portal",
  "tomislavnews", "hia.com.hr", "portalnovosti", "antenazadar",
  "dalmacijanews", "zadarskilist", "medjimurjepress",
  "zagreb.info", "034portal", "057info", "inmemoriam", "magicus.info",
  "book.hr", "mojzagreb.info", "skole.hr", "tvprofil", "cityportal",
  "klikaj.hr", "lika-online", "priznajem.hr", "ploce.com",
  "dragovoljac.com", "sbonline", "narod.hr", "infokiosk", "hrsvijet",
  "tomislavcity", "vrisak.info", "croatia", "anonymous_user", "reddit",
  "dalmacijadanas", "zupanjac.net", "dalmatinskiportal.hr", "campaign-archive.com",
  "županija", "zupanija", "kršćanska proročka crkva"
)

classify_actor_v2 <- function(from_value, url_value = NA) {
  from_lower <- tolower(from_value)
  url_lower <- tolower(ifelse(is.na(url_value), "", url_value))
  combined <- paste(from_lower, url_lower)
  
  # Helper function
  match_any <- function(patterns, text) {
    any(sapply(patterns, function(p) grepl(p, text, fixed = TRUE)))
  }
  
  # PRIORITY 1: Manual overrides
  for (actor_type in names(manual_overrides)) {
    if (match_any(manual_overrides[[actor_type]], from_lower)) {
      return(actor_type)
    }
  }
  
  # PRIORITY 2: Secular exclusions
  if (match_any(secular_media_exclusions, combined)) {
    return("Other")
  }
  
  institutional_domains <- c("hkm.hr", "ika.hkm.hr", "hkr.hkm.hr", "hbk.hr")
  if (any(sapply(institutional_domains, function(x) grepl(x, url_lower, fixed = TRUE)))) {
    return("Institutional Official")
  }
  
  institutional_names <- c(
    "hrvatska katolička mreža", "katolička mreža", "hkm", 
    "informativna katolička agencija", "ika", "hrvatski katolički radio", "hkr",
    "hrvatska biskupska konferencija", "hbk"
  )
  if (any(sapply(institutional_names, function(x) grepl(x, from_lower, fixed = TRUE)))) {
    return("Institutional Official")
  }
  
  diocesan_domains <- c(
    "zg-nadbiskupija.hr", "biskupija-varazdinska.hr", "djos.hr", 
    "biskupija-sj.hr", "rzs.hr", "rkc-sisak.hr", "zadarskanadbiskupija.hr",
    "gospicko-senjska-biskupija.hr", "nadbiskupija-split.com",
    "dubrovacka-biskupija.hr", "porec-biskupija.hr", "biskupija-kk.hr"
  )
  if (any(sapply(diocesan_domains, function(x) grepl(x, url_lower, fixed = TRUE)))) {
    return("Diocesan")
  }
  
  # Check for parish patterns
  is_parish <- grepl("^župa|^zupa|župi|zupi", from_lower, ignore.case = TRUE)
  diocesan_names <- c(
    "nadbiskupija", "biskupija", 
    "zagrebačka nadbiskupija", "splitsko-makarska", "đakovačko-osječka",
    "riječka nadbiskupija", "zadarska nadbiskupija", "sisačka biskupija",
    "varaždinska biskupija", "križevačka eparhija"
  )
  if (is_parish || any(sapply(diocesan_names, function(x) grepl(x, from_lower, fixed = TRUE)))) {
    return("Diocesan")
  }
  
  independent_media_exact <- c(
    "laudatotv", "laudato tv", "laudato.tv", "laudato.hr",
    "bitno.net", "bitno net", "glas koncila", "glaskoncila.hr",
    "nova-eva.com", "nova eva", "katolički tjednik", "katolicki tjednik",
    "kršćanska sadašnjost", "ks.hr", "verbum.hr", "verbum",
    "mir i dobro", "gfranciskovic", "kfranciskovic", "totus tuus"
  )
  if (any(sapply(independent_media_exact, function(x) grepl(x, from_lower, fixed = TRUE)))) {
    return("Independent Media")
  }
  
  independent_media_domains <- c(
    "laudato.hr", "laudato.tv", "bitno.net", "glaskoncila.hr", 
    "nova-eva.com", "verbum.hr", "ks.hr", "novizivot.net"
  )
  if (any(sapply(independent_media_domains, function(x) grepl(x, url_lower, fixed = TRUE)))) {
    return("Independent Media")
  }
  
  religious_orders_exact <- c(
    "franjevci", "franjevački", "franjevacki", "ofm",
    "isusovci", "družba isusova", "druzba isusova", "sj",
    "dominikanci", "dominikanski", "op",
    "salezijanci", "salezijanski", "sdb",
    "karmelićani", "karmelicani", "karmel",
    "benediktinci", "benediktinski", "osb",
    "kapucini", "kapucinski", "ofmcap",
    "pavlini", "pavlinski",
    "trapisti", "cisterciti",
    "sestre milosrdnice", "uršulinke", "klarise"
  )
  if (any(sapply(religious_orders_exact, function(x) grepl(x, from_lower, fixed = TRUE)))) {
    return("Religious Orders")
  }
  
  charismatic_exact <- c(
    "božja pobjeda", "bozja pobjeda", "bozjapobjeda",
    "srce isusovo", "srceisuovo", "srceisusovo",
    "muževni budite", "muzevni budite", "muzevnibudite",
    "molitvena zajednica", "karizmatska", "cenacolo",
    "emmanuel community", "emmanuel zajednica", "taize",
    "neokatekumenski", "neokatekumenska", "kursiljo",
    "fokolari", "fokolarini", "komunija i oslobođenje"
  )
  if (any(sapply(charismatic_exact, function(x) grepl(x, from_lower, fixed = TRUE)))) {
    return("Charismatic Communities")
  }
  
  # Check priest titles at start of name
  priest_prefixes <- c("fra ", "don ", "vlč.", "vlc.", "msgr.", "mons.",
                       "o. ", "pater ", "biskup ", "nadbiskup ", "kardinal ")
  for (prefix in priest_prefixes) {
    if (startsWith(from_lower, prefix)) return("Individual Priests")
  }
  if (grepl("svećenik|svecenik|župnik|zupnik", from_lower)) {
    return("Individual Priests")
  }
  
  youth_exact <- c(
    "frama", "shkm", "katolička mladež", "katolicka mladez",
    "ministranti", "mladifra", "mladi fra", "kaem", "kud",
    "sveučilišna kapelanija", "studentska kapelanija"
  )
  if (any(sapply(youth_exact, function(x) grepl(x, from_lower, fixed = TRUE)))) {
    return("Youth Organizations")
  }
  
  academic_exact <- c(
    "unicath", "katolički bogoslovni fakultet", "kbf",
    "teologija", "filozofski fakultet družbe isusove", "hks.hr",
    "hrvatsko katoličko sveučilište", "hku"
  )
  if (any(sapply(academic_exact, function(x) grepl(x, from_lower, fixed = TRUE)))) {
    return("Academic")
  }
  
  # Lay influencer detection
  lay_devotional <- c(
    "vjera", "molitva", "molitve", "isus", "krist", "gospa", "marija",
    "hrana za dušu", "dijete vjere", "duhovnost", "duhovna", "biblija",
    "psalm", "blagoslov", "krunica", "rozarij", "katolička obitelj"
  )
  lay_exclude <- c(".hr", ".net", ".com", "portal", "vijesti", "news", "radio")
  
  has_devotional <- any(sapply(lay_devotional, function(x) grepl(x, from_lower, fixed = TRUE)))
  has_media <- any(sapply(lay_exclude, function(x) grepl(x, from_lower, fixed = TRUE)))
  
  if (has_devotional && !has_media) {
    return("Lay Influencers")
  }
  
  return("Other")
}

dta[, ACTOR_TYPE := mapply(classify_actor_v2, FROM, URL)]

1.2 Corpus Overview

Show code
tibble(
  Metric = c("Total posts", "Posts with sufficient text (>50 chars)", "Unique sources", 
             "Date range", "Platforms"),
  Value = c(
    format(n_posts, big.mark = ","),
    format(n_with_text, big.mark = ","),
    format(uniqueN(dta$FROM), big.mark = ","),
    paste(min(dta$DATE), "to", max(dta$DATE)),
    paste(unique(dta$SOURCE_TYPE), collapse = ", ")
  )
) %>%
  kable(col.names = c("Metric", "Value")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Metric Value
Total posts 608,879
Posts with sufficient text (>50 chars) 608,228
Unique sources 16,426
Date range 2021-01-01 to 2025-12-31
Platforms web, facebook, twitter, comment, youtube, forum, reddit, instagram

2 Analysis 2.1: Topic Discovery

2.1 Text Preprocessing

Before topic modeling, text must be cleaned and tokenized. Preprocessing involves:

  1. URL removal: Eliminate web addresses that add noise
  2. Lowercasing: Standardize case for consistent matching
  3. Punctuation/number removal: Keep only alphabetic content
  4. Stopword removal: Filter common words that dont carry topical meaning
  5. Minimum character filter: Remove very short tokens (< 3 characters)
  6. Frequency trimming: Keep only terms appearing in multiple documents

Why stopwords matter: Croatian has rich morphology, so stopwords must cover all declensions and conjugations. The list includes auxiliaries (sam, si, je), prepositions (u, na, za), pronouns (ja, ti, on), and generic terms that appear across all topics.

Show code
croatian_stopwords <- unique(c(
  # --- CONJUNCTIONS (Veznici) ---
  "i", "pa", "te", "ni", "niti", "a", "ali", "nego", "no", "već", "vec",
  "ili", "da", "dok", "jer", "ako", "mada", "premda", "iako", "kako",
  "neka", "kad", "kada", "cim", "čim", "zato", "stoga", "dakle",

  # --- PREPOSITIONS (Prijedlozi) ---
  "u", "na", "s", "sa", "o", "po", "za", "uz", "iz", "do", "od", "pri",
  "k", "ka", "bez", "blizu", "osim", "među", "medju", "medu", "poput", "putem",
  "prema", "pored", "pokraj", "kroz", "nad", "pod", "pred", "oko", "okolo",
  "niz", "uzduž", "duž", "vrh", "dno", "kraj", "krajem", "usprkos", "glede",
  "nasuprot", "ispod", "iznad", "između", "izmedju", "iza", "unutar", "van",

  # --- AUXILIARY VERBS & "BITI" (To Be) ---
  "sam", "si", "je", "smo", "ste", "su", "jesam", "jesi", "jest", "jesmo", "jeste", "jesu",
  "budem", "budeš", "budes", "bude", "budemo", "budete", "budu",
  "bio", "bila", "bilo", "bili", "bile",
  "nisam", "nisi", "nije", "nismo", "niste", "nisu",
  "ne", "nemoj", "nemojte", "nemojmo",
  "biti", "bit", "bismo", "biste",

  # --- VERBS "HTJETI" (Will/Want) ---
  "ću", "cu", "ćeš", "ces", "će", "ce", "ćemo", "cemo", "ćete", "cete",

  "hoću", "hocu", "hoćeš", "hoces", "hoće", "hoce", "hoćemo", "hocemo", "hoćete", "hocete",
  "bih", "bi", "htio", "htjela", "htjelo", "htjeli", "htjele",
  "neću", "necu", "nećeš", "neces", "neće", "nece", "nećemo", "necemo", "nećete", "necete",

  # --- VERBS "MOĆI" & "IMATI" (Can/Have) ---
  "mogu", "možeš", "mozes", "može", "moze", "možemo", "mozemo", "možete", "mozete",
  "mogao", "mogla", "moglo", "mogli", "mogle", "moći", "moci",
  "imam", "imaš", "imas", "ima", "imamo", "imate", "imaju", "imao", "imala", "imalo", "imali", "imati",

  # --- COMMON VERBS (Česti glagoli) ---
  "reći", "reci", "rekao", "rekla", "rekli", "rekle", "kaže", "kaze", "kazao", "kazala",
  "treba", "trebao", "trebala", "trebali", "trebale", "trebati",
  "mora", "moram", "moraš", "moras", "moramo", "morate", "moraju", "morao", "morala", "morali",
  "želi", "zeli", "želim", "zelim", "želite", "zelite", "želio", "zelio", "željela", "zeljela",
  "zna", "znam", "znaš", "znas", "znamo", "znate", "znaju", "znao", "znala", "znali",
  "vidi", "vidim", "vidiš", "vidis", "vidimo", "vidite", "vide", "vidio", "vidjela", "vidjeli",
  "dati", "dao", "dala", "dali", "dale", "dajem", "daješ", "daje", "dajemo", "dajete", "daju",
  "uzeti", "uzeo", "uzela", "uzeli", "uzimam", "uzimaš", "uzima",
  "ići", "ici", "išao", "isao", "išla", "isla", "išli", "isli", "idem", "ideš", "ide",
  "doći", "doci", "došao", "dosao", "došla", "dosla", "došli", "dosli", "dolazi", "dolazim",
  "raditi", "radio", "radila", "radili", "radim", "radiš", "radi", "radimo", "radite", "rade",
  "pisati", "pisao", "pisala", "pisali", "pišem", "pisem", "piše", "pise",
  "čitati", "citati", "čitao", "citao", "čitala", "citala", "čitam", "citam",
  "staviti", "stavio", "stavila", "stavili", "stavljam", "stavlja",
  "nastaviti", "nastavio", "nastavila", "nastavili", "nastavlja", "nastavljam",
  "postati", "postao", "postala", "postali", "postaje", "postajem",

  # --- PRONOUNS: All persons and cases ---
  "ja", "mene", "meni", "me", "mnom", "mnome",
  "mi", "nas", "nama", "nam",
  "ti", "tebe", "tebi", "te", "tobom", "tobome",
  "vi", "vas", "vama", "vam",
  "on", "njega", "njemu", "ga", "nj", "njim", "njime",
  "ona", "nje", "njoj", "ju", "njom", "njome",
  "ono",
  "oni", "one", "njih", "njima", "ih",
  "sebe", "sebi", "se", "sobom",

  # --- DETERMINERS & POSSESSIVES ---
  "taj", "ta", "to", "tog", "toga", "tom", "tome", "tomu", "tim", "tima", "toj", "te",
  "ovaj", "ova", "ovo", "ovog", "ovoga", "ovom", "ovome", "ovim", "ovima", "ovoj", "ove",
  "onaj", "onog", "onoga", "onom", "onome", "onim", "onima", "onoj",
  "moj", "moja", "moje", "mojeg", "mojega", "mom", "mome", "mojem", "mojemu", "mojim", "mojima", "mojih",
  "tvoj", "tvoja", "tvoje", "tvojeg", "tvojim",
  "naš", "naša", "nasa", "naše", "nase", "našeg", "naseg", "našem", "nasem", "našim", "nasim",
  "vaš", "vaša", "vasa", "vaše", "vase", "vašeg", "vaseg", "vašem", "vasem", "vašim", "vasim",
  "njegov", "njegova", "njegovo", "njen", "njezin", "njihov", "njihova", "njihovo",
  "svoj", "svoja", "svoje", "svojeg", "svojem", "svojim", "svojih", "svojima",

  # --- RELATIVE PRONOUNS / QUESTION WORDS ---
  "tko", "što", "sto", "kog", "koga", "kom", "kome", "čemu", "cemu", "čim", "cim", "čime", "cime",
  "koji", "koja", "koje", "kojeg", "kojega", "kojem", "kojemu", "kojim", "kojima", "kojih",
  "kakav", "kakva", "kakvo", "svaki", "svaka", "svako", "sva", "sve", "svi", "svih", "svima",
  "neki", "neka", "neko", "nekog", "nekom", "nekim", "nekih", "nekima",
  "sav", "cijeli", "cijela", "cijelo",
  "netko", "nešto", "nesto", "nitko", "ništa", "nista", "svatko", "svašta", "svasta",
  "isti", "ista", "isto", "istog", "istom", "istim",

  # --- ADVERBS & PARTICLES ---
  "gdje", "gde", "kamo", "kuda", "odakle", "kako", "zašto", "zasto",
  "ovdje", "ovde", "tamo", "tu", "negdje", "negde", "nigdje", "nigde",
  "jučer", "jucer", "danas", "sutra", "sada", "sad", "tada", "onda", "uvijek", "uvek", "nikad", "nikada",
  "mnogo", "puno", "malo", "više", "vise", "manje", "dosta", "prilično", "prilicno",
  "jako", "vrlo", "veoma", "baš", "bas", "čak", "cak", "tek", "još", "jos",
  "dakle", "međutim", "medjutim", "naprosto", "naime", "vjerojatno", "naravno", "sigurno", "zaista", "doista",
  "možda", "mozda", "ipak", "uostalom", "uglavnom", "najčešće", "najcesce", "ponekad", "rijetko", "retko",
  "također", "takodjer", "takodje", "isto", "također", "zatim", "potom", "konačno", "konacno",
  "prije", "pre", "poslije", "posle", "tijekom", "tokom", "dok", "kada", "čim", "cim",
  "gotovo", "skoro", "upravo", "jedino", "samo", "posebno", "osobito", "naročito", "narocito",
  "dobro", "loše", "lose", "brzo", "polako", "sporo", "lako", "teško", "tesko",
  "odmah", "uskoro", "kasno", "rano", "odavno", "nedavno", "opet", "ponovno", "ponovo",

  # --- NUMBERS ---
  "jedan", "jedna", "jedno", "jednog", "jednoj", "jednom", "jednim",
  "dva", "dvije", "dvoje", "dvaju", "dvjema",
  "tri", "troje", "triju", "trima",
  "četiri", "cetiri", "četvero", "cetvero",
  "pet", "šest", "sest", "sedam", "osam", "devet", "deset",
  "jedanaest", "dvanaest", "trinaest", "dvadeset", "trideset", "sto", "tisuću", "tisucu", "milijun",
  "prvi", "prva", "prvo", "prvog", "prvoj", "prvom",
  "drugi", "druga", "drugo", "drugog", "drugoj", "drugom", "drugim",
  "treći", "treci", "treća", "treca", "treće", "trece",
  "četvrti", "cetvrti", "peti", "šesti", "sesti",

  # --- TIME EXPRESSIONS ---
  "godina", "godine", "godinu", "godini", "godinama", "godišnje", "godisnje",
  "mjesec", "mjeseca", "mjeseci", "mjesecu",
  "tjedan", "tjedna", "tjedni", "tjednu",
  "dan", "dana", "danu", "dani", "danima", "dnevno",
  "sat", "sata", "sati", "satu", "satima",
  "minuta", "minute", "minutu", "minuti",
  "vrijeme", "vremena", "vremenu",
  "početak", "pocetak", "početka", "pocetka", "kraj", "kraja", "kraju",
  "ponedjeljak", "utorak", "srijeda", "četvrtak", "cetvtak", "petak", "subota", "nedjelja",
  "siječanj", "sijecanj", "veljača", "veljaca", "ožujak", "ozujak", "travanj", "svibanj",
  "lipanj", "srpanj", "kolovoz", "rujan", "listopad", "studeni", "prosinac",

  # --- TECH / WEB NOISE ---
  "http", "https", "www", "com", "hr", "org", "net", "foto", "video", "slika", "clanak", "članak",
  "link", "klik", "stranica", "stranici", "stranice", "web", "online", "internet",
  "email", "mail", "telefon", "mob", "fax",
  "cookie", "cookies", "kolačić", "kolacic", "kolačića", "kolacica", "kolačiće", "kolacice",
  "kolačićima", "kolacicima", "postavke", "podataka", "podatke", "pristupačnosti", "pristupacnosti",
  "privatnosti", "google", "facebook", "youtube", "instagram", "twitter",
  "user", "article", "category", "consent", "postovi", "postano", "pridružen", "pridruzen",
  
  # --- MEDIA/NEWS NOISE ---
  "foto", "video", "izvor", "autor", "objavljeno", "objavio", "objavila",
  "pročitajte", "procitajte", "pogledajte", "kliknite", "saznajte",
  "cropix", "afp", "hina", "reuters", "pixsell",
  "nastavak", "nastavi", "više", "vise", "detaljnije", "opširnije", "opsirnije",

  # --- COMMON NOUNS TO EXCLUDE ---
  "čovjek", "covjek", "čovjeka", "covjeka", "ljudi", "osoba", "osobe", "osobu", "osoba",
  "stvar", "stvari", "stvarima", "način", "nacin", "načina", "nacina", "načinom", "nacinom",
  "pitanje", "pitanja", "pitanju", "odgovor", "odgovora", "odgovoru",
  "dio", "dijela", "dijelu", "dijelovi", "dijelom",
  "put", "puta", "putu", "putevi", "putem",
  "mjesto", "mjesta", "mjestu", "mjestima",
  "strana", "strane", "stranu", "strani",
  "slučaj", "slucaj", "slučaja", "slucaja", "slučaju", "slucaju",
  "primjer", "primjera", "primjeru", "primjerice",
  "razlog", "razloga", "razlogu", "razlozi",
  "činjenica", "cinjenica", "činjenice", "cinjenice",
  "podatak", "podatka", "podaci", "podataka",
  "informacija", "informacije", "informaciju",
  "tema", "teme", "temu", "temi",
  "problem", "problema", "problemu", "problemi",
  "rješenje", "rjesenje", "rješenja", "rjesenja",
  "rezultat", "rezultata", "rezultatu", "rezultati",
  "cilj", "cilja", "cilju", "ciljevi",
  "uvjet", "uvjeta", "uvjetu", "uvjeti",
  "mogućnost", "mogucnost", "mogućnosti", "mogucnosti",
  "potreba", "potrebe", "potrebu", "potrebi",
  "sustav", "sustava", "sustavu", "sustavi",
  "proces", "procesa", "procesu", "procesi",
  "program", "programa", "programu", "programi",
  "projekt", "projekta", "projektu", "projekti",
  "broj", "broja", "broju", "brojevi",
  "red", "reda", "redu",
  "vrsta", "vrste", "vrstu", "vrsti",
  "oblik", "oblika", "obliku", "oblici",
  "razina", "razine", "razinu", "razini",
  "stupanj", "stupnja", "stupnju",
  "temelj", "temelja", "temelju",
  "osnova", "osnove", "osnovu", "osnovi",
  
  # --- ADJECTIVES (common/generic) ---
  "velik", "velika", "veliko", "veliki", "veliku", "velikog", "velikom",
  "mali", "mala", "malo", "malog", "malom", "malim",
  "novi", "nova", "novo", "novog", "novoj", "novom", "novih", "novima",
  "star", "stara", "staro", "stari", "starog", "starom",
  "dobar", "dobra", "dobro", "dobrog", "dobroj", "dobrom", "dobrim",
  "loš", "los", "loša", "losa", "loše", "lose",
  "pravi", "prava", "pravo", "pravog", "pravoj", "pravom",
  "određen", "odreden", "određena", "odredena", "određeno", "odredeno",
  "poseban", "posebna", "posebno", "posebnog", "posebnoj",
  "različit", "razlicit", "različita", "razlicita", "različito", "razlicito",
  "sličan", "slican", "slična", "slicna", "slično", "slicno",
  "moguć", "moguc", "moguća", "moguca", "moguće", "moguce",
  "potreban", "potrebna", "potrebno", "potrebnog", "potrebnoj",
  "važan", "vazan", "važna", "vazna", "važno", "vazno", "važnog", "vaznog",
  "jednak", "jednaka", "jednako", "jednakog",
  "drukčiji", "drukciji", "drukčija", "drukcija", "drukčije", "drukcije",
  "posljednji", "posljednja", "posljednje", "zadnji", "zadnja", "zadnje",
  "sljedeći", "slijedeci", "sljedeća", "sljedeca", "sljedeće", "sljedece",
  "prethodni", "prethodna", "prethodno", "prijašnji", "prijasnji",
  "sadašnji", "sadasnji", "sadašnja", "sadasnja", "sadašnje", "sadasnje",
  "buduć", "buduc", "buduća", "buduca", "buduće", "buduce", "budući", "buduci",
  
  # --- LINKING/DISCOURSE WORDS ---
  "kao", "poput", "primjerice", "recimo", "odnosno", "naime", "zapravo",
  "prije", "nakon", "tijekom", "tokom", "dok", "čim", "cim",
  "osim", "osim toga", "uz to", "pored toga", "unatoč", "unatoc", "usprkos",
  "zbog", "radi", "poradi", "uslijed", "usled",
  "tako", "takav", "takva", "takvo", "takvog", "takvoj", "takvom",
  "ovako", "onako",
  "često", "cesto", "rijetko", "retko", "ponekad", "katkad", "uvijek", "uvek",
  "nikad", "nikada", "negdje", "negde", "nigdje", "nigde", "svugdje", "svugde",
  "otprilike", "približno", "priblizno", "točno", "tocno", "upravo",
  "barem", "baren", "najmanje", "najviše", "najvise", "potpuno", "sasvim", "posve",
  
  # --- SPAM/ADVERTISING NOISE ---
  "mršavljenje", "mrsavljenje", "tablete", "dijeta", "kilograma", "kožu", "kozu", "maska",
  "oil", "name", "can", "black", "make", "best", "free", "click", "buy", "sale",
  "euro", "eura", "kuna", "kn", "eur", "usd",
  
  # --- FOREIGN WORDS (German/English artifacts) ---
  "der", "die", "das", "und", "von", "den", "nach", "mit", "aus", "bei",
  "the", "and", "for", "you", "are", "was", "that", "this", "with", "have",
  
  # --- NEWS AGENCY/SOURCE MARKERS ---
  "cropix", "afp", "reuters", "hina", "pixsell", "epa", "ap", "dpa",
  "tportal", "quote", "update", "breaking",
  
  # --- TRAVEL/TOURISM NOISE ---
  "noćenje", "nocenje", "aranžman", "aranzman", "hotel", "smještaj", "smjestaj",
  "putovanje", "putovanja", "razgled", "tura", "turista",
  
  # --- TECH/CODE NOISE ---
  "swift", "banka", "code", "tube", "app", "software", "download", "upload",
  "username", "password", "login", "account",
  
  # --- LOCATION GENERIC ---
  "grad", "grada", "gradu", "gradovi", "selo", "sela", "selu",
  "ulica", "ulice", "ulici", "ulicu",
  "područje", "podrucje", "područja", "podrucja", "regija", "regije",
  
  # --- ADDITIONAL HIGH-FREQUENCY NOISE FROM YOUR OUTPUT ---
  "kontakt", "kontakta", "kontaktu",
  "prijava", "prijave", "prijavu", "prijavi", "rok", "roka", "roku",
  "hvala", "molim", "molimo",
  "škola", "skola", "škole", "skole", "školu", "skolu",
  "udruga", "udruge", "udruzi", "udrugu"
))


dta_text <- dta[!is.na(FULL_TEXT) & nchar(FULL_TEXT) > 100]

set.seed(42)

# Use SAMPLE_PCT from config (set at top of file)
sample_size <- ceiling(nrow(dta_text) * SAMPLE_PCT)
sample_idx <- sample(1:nrow(dta_text), sample_size)
dta_sample <- dta_text[sample_idx]

cat("
╔══════════════════════════════════════════════════════════════════╗
║  SAMPLING CONFIGURATION                                          ║
╠══════════════════════════════════════════════════════════════════╣
║  Sample percentage:", sprintf("%5.1f%%", SAMPLE_PCT * 100), "
║  Documents sampled:", sprintf("%6d", nrow(dta_sample)), "
║  Total available:  ", sprintf("%6d", nrow(dta_text)), "
╚══════════════════════════════════════════════════════════════════╝
")

╔══════════════════════════════════════════════════════════════════╗
║  SAMPLING CONFIGURATION                                          ║
╠══════════════════════════════════════════════════════════════════╣
║  Sample percentage:   1.0% 
║  Documents sampled:   6046 
║  Total available:   604583 
╚══════════════════════════════════════════════════════════════════╝
Show code
dta_sample[, doc_id := .I]
dta_sample[, clean_text := FULL_TEXT]
dta_sample[, clean_text := gsub("https?://\\S+", "", clean_text)]
dta_sample[, clean_text := gsub("www\\.\\S+", "", clean_text)]
dta_sample[, clean_text := gsub("[^[:alpha:][:space:]]", " ", clean_text)]
dta_sample[, clean_text := tolower(clean_text)]
dta_sample[, clean_text := gsub("\\s+", " ", clean_text)]
dta_sample[, clean_text := trimws(clean_text)]
Show code
corpus_cath <- corpus(dta_sample, text_field = "clean_text", docid_field = "doc_id")

docvars(corpus_cath, "actor_type") <- dta_sample$ACTOR_TYPE
docvars(corpus_cath, "platform") <- dta_sample$SOURCE_TYPE
docvars(corpus_cath, "year") <- dta_sample$year

tokens_cath <- tokens(corpus_cath, 
                      remove_punct = TRUE,
                      remove_numbers = TRUE,
                      remove_symbols = TRUE) %>%
  tokens_tolower() %>%
  tokens_remove(pattern = croatian_stopwords) %>%
  tokens_remove(pattern = stopwords("en")) %>%
  tokens_keep(min_nchar = 3)

dfm_cath <- dfm(tokens_cath) %>%
  dfm_trim(min_termfreq = 10, min_docfreq = 5)

cat("Documents:", ndoc(dfm_cath), "\n")
Documents: 6046 
Show code
cat("Features:", nfeat(dfm_cath), "\n")
Features: 28829 
Show code
# Safety checks before running STM
if (ndoc(dfm_cath) == 0) {
  stop("DFM has 0 documents! Check your data loading and filtering steps.")
}
if (nfeat(dfm_cath) == 0) {
  stop("DFM has 0 features! Check your text preprocessing and trimming parameters.")
}
if (ndoc(dfm_cath) < 100) {
  warning("Very few documents (", ndoc(dfm_cath), "). Results may be unreliable.")
}

stm_dfm <- convert(dfm_cath, to = "stm")

# Verify conversion worked
if (length(stm_dfm$documents) == 0) {
  stop("STM conversion resulted in 0 documents!")
}

# Use NUM_TOPICS from config (set at top of file)
K <- NUM_TOPICS

# Adjust K if we have too few documents
if (length(stm_dfm$documents) < K * 3) {
  K <- max(5, floor(length(stm_dfm$documents) / 3))
  warning("Reduced K to ", K, " due to small corpus size")
}

stm_model <- stm(
  documents = stm_dfm$documents,
  vocab = stm_dfm$vocab,
  K = K,
  prevalence = ~ actor_type + platform,
  data = stm_dfm$meta,
  init.type = "Spectral",
  max.em.its = 50,
  verbose = FALSE,
  seed = 42
)

cat("STM model fitted with", K, "topics\n")
STM model fitted with 35 topics

2.2 Topic Labels and Top Words

Each topic is characterized by words that appear frequently within it. We present two types of word rankings:

Top Words (Probability): Words with highest probability of appearing in documents assigned to this topic. These are the most common terms but may appear across multiple topics.

FREX Words (Distinctive): Words that are both frequent AND exclusive to this topic. FREX (Frequency and Exclusivity) scoring helps identify what makes each topic unique. These are typically more useful for interpreting topic content.

How to read the table:

  • Look at FREX words first to understand what distinguishes each topic
  • Topics with clear thematic coherence (e.g., all words related to liturgy) are well defined
  • Topics with mixed words may capture multiple related themes or require further refinement
Show code
topic_words <- labelTopics(stm_model, n = 10)

topic_df <- data.frame(
  Topic = 1:K,
  Top_Words = apply(topic_words$prob, 1, paste, collapse = ", "),
  FREX_Words = apply(topic_words$frex, 1, paste, collapse = ", ")
)

topic_df %>%
  kable(col.names = c("Topic", "Top Words (Probability)", "FREX Words (Distinctive)")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = TRUE) %>%
  scroll_box(height = "600px")
Topic Top Words (Probability) FREX Words (Distinctive)
1 koncert, glazbe, festival, festivala, pjesme, prosinca, večer, glazbu, zbor, hrvatske koncert, glazbe, koncertu, orkestar, bogdanić, glazbu, skladbe, koncerata, glazbi, bojan
2 film, filma, filmu, min, priča, svijet, filmova, gubitak, kino, najbolji film, ufc, section, filmova, filma, filmu, serija, dokumentarni, kino, used
3 međugorje, molitva, atma, marija, krunice, molitve, međugorju, gospa, rad, krunicu međugorje, međugorju, atma, ukazanja, krunicu, krunica, međugorja, krunice, molitveni, iban
4 učenici, hrvatske, muzeja, učenika, aktivnosti, djeca, djecu, školi, rada, obrazovanja razred, učenici, mentorica, čipke, knjižnica, razreda, učenike, školskog, natjecanje, natjecanja
5 fra, svećenik, svećenika, don, rođen, župnik, crkvi, franjevaca, zagrebu, provincije franjevaca, zaređen, provincijal, svećenik, fra, franjevac, franjevci, provincije, rođen, franjevačke
6 bog, isus, boga, bogu, život, ljubavi, molitva, božje, ljubav, isusa amen, biblija, evanđelje, spasenje, isuse, boga, božja, gospodine, oče, moli
7 kod, trgu, cesta, ispred, kbr, ivana, trg, subotu, prometa, promet kbr, ulicom, promet, cesta, adventske, državna, prometa, fridrih, raskrižja, adventa
8 ukrajini, trump, rat, rata, predsjednik, ukrajine, protiv, ruske, zemlje, rusija rusija, ruske, zelenski, trump, ukrajinu, putin, ukrajinske, ruska, rusije, ukrajina
9 stoljeća, hrvatske, povijest, ime, povijesti, hrvatski, nalazi, crkve, baštine, muzeja zbirke, slikar, dvorac, zbirka, prezimena, vodnjan, vijeka, dvorca, zvono, zvonik
10 sjećanja, vukovara, rata, žrtve, vukovar, domovinskog, branitelja, predsjednik, hrvatskih, spomen vukovar, vukovara, škabrnje, sjećanja, škabrnji, vukovaru, komemoracija, mojsijeva, žrtvama, četvrt
11 susret, mladih, nadbiskupije, vlč, đakovačko, biskupije, pomoć, ika, susreta, osječke đakovačko, osječke, pastoral, đakovu, hranića, susret, potresu, župnih, vjeroučitelje, stipić
12 plenković, predsjednik, hdz, hrvatske, hrvatska, milanović, predsjednika, premijer, hrvatskoj, vlade plenković, hdz, milanović, andrej, primorac, premijer, milanovića, sdp, izborima, raspudić
13 ljude, nema, protiv, neke, postoji, svijetu, druge, svijet, život, mislim virus, virusa, cjepiva, muslimani, cjepivo, prijevoda, maske, cijepljenje, islam, prijevod
14 obitelji, supruga, princ, crkvi, kraljica, charles, kralj, vijest, london, sin princ, charles, princeza, lijes, javljamo, string, priznajem, tužnu, znancima, ožalošćeni
15 joj, koliko, nema, žena, priča, život, koju, kod, mislim, dalje mama, šta, sjećam, shvatila, nemam, volim, muž, nekako, tata, kažem
16 žrtava, rata, pokolj, kod, nema, jasenovac, nekoliko, hrvatske, djece, preko pokolj, jasenovac, logora, pavković, jasenovcu, tito, soldo, krleža, logor, partizani
17 hrvatske, hrvatski, hrvatska, hrvatskoj, hrvata, naroda, hrvatsku, hrvatskog, hrvati, narod srpskog, ndh, hrvatima, snv, manjine, pupovac, vučić, ustavni, srba, antifašizam
18 papa, franjo, pape, vatikan, franje, kardinal, papu, crkve, vatikanu, svete vatikan, papa, xiv, vatican, vatikanu, papu, franjo, vatikana, gemelli, lav
19 zadru, zadar, zadarske, zadra, zadarski, života, šime, zgrablić, sveučilišta, život zadar, zadru, zadra, zadarske, zadarska, stošije, zadarskoj, zadarski, zadarskog, zgrablić
20 knjige, znanosti, knjiga, umjetnosti, sveučilišta, zagreb, str, zagrebu, prof, knjigu str, galović, tehnike, des, znanosti, teologija, meme, une, umjetnosti, pour
21 misa, gospe, župe, mise, don, slavlje, župnik, crkvi, misu, fra laudato, svetištu, svetište, župa, misno, hodočašće, blažene, sinjske, župe, uznesenja
22 obitelji, života, sestre, dijete, djece, život, ljubavi, sestara, djeca, djecu djeteta, sestara, ccaron, sestre, krstio, cacute, krštenje, dijete, krizme, sestrama
23 pristup, pravila, kolačići, koristiti, svrhu, koristi, koristimo, sadržaja, osobnih, korisnika kolačići, spremiti, neophodni, tehničko, korisnik, skladište, našice, disable, pretplatnik, gdpr
24 razvoj, proizvoda, posto, hrvatskoj, vozila, hrvatska, tržištu, tvrtke, energije, pristup poslovanja, potrošača, kwh, tržištu, inovacije, održivosti, inovacija, goriva, brend, tržišta
25 županije, općine, području, županija, gradonačelnik, milijuna, župan, općina, županiji, vijeća sisačko, krapinsko, županija, međimurska, župana, županijske, toplice, moslavačke, županiji, labin
26 protiv, nema, žene, žena, kojoj, novac, koju, riječ, neke, dalje mirovine, rimac, polovica, plaće, sektoru, polovici, mirovinskog, banke, kapitala, pobačaj
27 hrvatska, isusu, bogu, bog, isus, život, pretplatite, društvenim, mrežama, kralju pretplatite, epizode, zvonce, klipovi, dovijeka, copyright, kralju, dijelite, igrača, music
28 život, ljubav, života, koju, ljubavi, svoju, svijet, misli, osjećaj, životu energija, sposobnost, osjećate, emocije, emocionalno, patnja, savjet, bića, uma, osjećaj
29 hrvatske, zagreb, prof, hrvatska, izložba, zagrebu, hrvatskih, vijesti, varaždin, hrvatski camino, dinamo, varaždin, varaždina, blaženika, predstavljena, izložbu, oratorij, predstavljanje, mhz
30 hrvatske, branitelja, hrvatskih, rata, domovinskog, hrvatskog, spomen, policije, županije, republike brigade, domovinske, bojne, poginulim, izaslanik, braniteljima, policije, gardijske, vijenaca, vojno
31 biskup, biskupije, mons, caritasa, biskupa, biskupija, katedrali, caritas, radoš, dubrovačke radoš, biskup, košić, caritasa, glasnović, sisački, biskupija, gospićko, dubrovačke, biskupu
32 crkve, crkva, crkvi, katoličke, crkvu, vjere, stepinac, pravoslavne, stepinca, vjernika pravoslavne, koncila, stepinac, nauk, patrijarh, pravoslavna, stepincu, katoličkih, biskupi, spc
33 cijena, nalazi, kuća, voda, ulja, otok, vina, otoka, prirode, vodi najniža, plaža, staza, mirisa, jelo, restorana, okusa, restoran, plaže, ulja
34 nadbiskup, mons, nadbiskupa, kardinal, nadbiskupije, kutleša, ocijeni, zagrebački, puljić, istaknuo ocijeni, kutleša, bozanić, puljić, grakalić, nadbiskup, uzinić, metropolit, vrhbosanski, nadbiskupa
35 božić, blagdan, uskrs, božića, slavi, krista, isusa, uskrsa, vjernici, smrti uskrsa, uskrs, božić, običaj, vazmeno, uskrsnuća, kristova, božića, badnjak, korizme

2.3 Topic Prevalence Overview

Topic prevalence measures what proportion of the corpus is devoted to each topic. In STM, each document is modeled as a mixture of topics, so prevalence represents the average proportion across all documents.

Interpretation:

  • Higher prevalence topics represent dominant themes in the corpus
  • The distribution typically follows a long tail pattern, with a few dominant topics and many niche ones
  • Prevalence alone does not indicate importance; niche topics may be highly significant for specific actor types or contexts
Show code
topic_proportions <- colMeans(stm_model$theta)

topic_prev_df <- data.frame(
  Topic = factor(1:K),
  Prevalence = topic_proportions * 100
) %>%
  arrange(desc(Prevalence)) %>%
  mutate(Topic = factor(Topic, levels = Topic))

ggplot(topic_prev_df, aes(x = Topic, y = Prevalence)) +
  geom_col(fill = "#2c5f7c", width = 0.7) +
  geom_text(aes(label = sprintf("%.1f%%", Prevalence)), 
            hjust = -0.1, size = 3) +
  coord_flip() +
  scale_y_continuous(limits = c(0, max(topic_prev_df$Prevalence) * 1.15),
                     labels = function(x) paste0(x, "%")) +
  labs(
    title = "Topic Prevalence in Corpus",
    subtitle = paste("Distribution across", K, "topics"),
    x = "Topic",
    y = "Prevalence (%)"
  )

2.4 Topic Word Clouds

Show code
par(mfrow = c(5, 7), mar = c(0.5, 0.5, 1.5, 0.5))

for (i in 1:K) {
  cloud(stm_model, topic = i, max.words = 30, 
        colors = brewer.pal(8, "Dark2"),
        main = paste("Topic", i))
}

Show code
par(mfrow = c(1, 1))

3 Analysis 2.2: Topic Taxonomy

Group discovered topics into higher order thematic categories based on manual inspection of top words.

3.1 Automated Category Assignment

To organize the 35 discovered topics into meaningful higher level categories, we use keyword matching against predefined thematic dictionaries. Each topic is assigned to the category with the most keyword matches among its top and FREX words.

Category definitions:

Category Description Example keywords
Liturgical/Sacramental Mass, sacraments, liturgical calendar misa, sakrament, euharistij, uskrs
Devotional Prayer, saints, personal piety molitva, gospa, svetac, duhov
Institutional Church hierarchy, appointments, governance biskup, papa, vatikan, imenovan
Social/Ethical Family, charity, social issues obitelj, caritas, siromašn, brak
Political Politics, history, national identity vlada, hrvat, rat, domovina
Youth/Community Youth ministry, community activities mladi, frama, kamp, zajednic
Educational Catechesis, theology, learning kateh, teolog, biblij, učenj

Topics that do not match any category are labeled Other. This automatic classification provides a starting point; manual refinement may be needed for edge cases.

Show code
topic_taxonomy <- data.frame(
  Topic = 1:K,
  stringsAsFactors = FALSE
)

topic_taxonomy$Category <- NA
topic_taxonomy$Label <- NA

topic_words_list <- lapply(1:K, function(i) {
  c(topic_words$prob[i, ], topic_words$frex[i, ])
})

# EXPANDED KEYWORD DICTIONARIES
# Each list uses partial matching so "euharistij" matches "euharistija", "euharistije", etc.

liturgical_keywords <- c(
  # Mass and liturgy
  "misa", "mise", "misi", "misom", "misno", "misu",
  "liturgi", "euharistij", "pričest", "pricest", "hostij",
  "oltarsk", "sakrament", "obreda", "slavlje", "slavlj",
  # Sacraments
  "krizm", "krst", "krštenje", "krstenje", "ispovijed", "ispovjed",
  "pomirenje", "pomazanje", "ređenje", "redjenje", "zaruk",
  # Liturgical calendar
  "uskrs", "uskrsn", "božić", "bozic", "advent", "korizma", "vazmeni",
  "došašć", "dosasć", "blagdan", "svetkovina", "nedjelj",
  # Liturgical objects/actions
  "procesij", "klanjanje", "klanja", "blagoslov", "posveta", "krizni",
  "pjevanje", "ministrant", "čitanje", "citanje", "propovijed"
)

devotional_keywords <- c(
  # Prayer
  "molitv", "molit", "molim", "krunic", "rozarij", "krunicu",
  "zavjetni", "pobožn", "pobozn", "duhovn", "meditacij",
  # Mary/Saints
  "gospa", "gospe", "gospi", "marij", "majka", "majci",
  "svetac", "svetica", "sveti", "svetog", "svetoj", "blažen", "blazen",
  "relikvij", "hodočašć", "hodocasc", "štovanje", "stovanje",
  # Faith/spirituality
  "vjera", "vjere", "vjeri", "vjeru", "vjernik",
  "isus", "isusa", "isusu", "krist", "krista", "kristu",
  "srce", "srca", "srcu", "ljubav", "ljubavi", "milost",
  "duh", "duha", "duhu", "sveti duh", "spasenje", "obraćenj"
)

institutional_keywords <- c(
  # Hierarchy
  "papa", "papu", "pape", "papom", "papinsk", "franjo",
  "kardinal", "nadbiskup", "biskup", "biskupij", "biskupov",
  "vatikan", "vatikanu", "sveta stolica", "hbk", "koncil",
  # Governance
  "imenovan", "razriješen", "razrijesen", "dekret", "pastoralni",
  "sinod", "koadjutor", "metropolit", "ordinarij", "vikari",
  # Parish
  "župnik", "zupnik", "župi", "zupi", "župe", "zupe", "župan",
  "župno", "zupno", "dekanat", "provincij"
)

social_keywords <- c(
  # Family
  "obitelj", "obitelji", "brak", "braka", "braku", "bračn", "bracn",
  "djeca", "djece", "djeteta", "dijete", "majka", "otac", "roditelj",
  # Life issues
  "pobačaj", "pobacaj", "abortus", "nerođen", "nerodjen", "eutanazij",
  "život", "zivot", "života", "zivota", "rađanje", "radjanje",
  # Charity/social
  "caritas", "caritasa", "siromašn", "siromasn", "pomoć", "pomoc",
  "socijal", "humanitarn", "volonter", "dobrotvor", "donacij",
  # Social issues
  "društv", "drustv", "zajednic", "solidarnost", "pravednost",
  "siromaštv", "siromaštvo", "beskućn", "beskucn", "migranti", "izbjegl"
)

political_keywords <- c(
  # Politics
  "vlada", "vlade", "vladi", "sabor", "sabora", "parlament",
  "politič", "politic", "stranka", "stranke", "izbor", "izbora",
  "predsjednik", "ministar", "hdz", "sdp", "plenković", "milanov",
  # History
  "rat", "rata", "ratu", "ratni", "domovinski", "vukovar",
  "jasenovac", "bleiburg", "pokolj", "genocid", "zločin", "zlocin",
  "komunis", "totalitar", "ndh", "ustašk", "ustask", "partizan",
  # National identity
  "hrvat", "hrvatsk", "domovina", "domoljub", "narod", "nacija",
  "državni", "drzavni", "republika", "suverenitet"
)

youth_keywords <- c(
  # Youth organizations
  "mladi", "mladih", "mladež", "mladez", "frama", "shkm",
  "ministrant", "ministranti", "ministranata",
  # Activities
  "kamp", "kampa", "kampu", "susret", "susreta", "susreti",
  "animatori", "animator", "voditelj", "program", "aktivnost",
  # Education
  "škola", "skola", "škole", "skole", "učenj", "ucenj",
  "student", "studenta", "fakultet", "sveučilište", "sveuciliste",
  "kapelanij", "pastoral", "vjeronauk"
)

educational_keywords <- c(
  # Catechesis
  "kateh", "katehet", "katekiz", "vjeronauk", "pouka", "poduk",
  # Theology
  "teolog", "teološk", "teoloski", "bogoslov", "egzeges",
  # Bible
  "biblij", "svetog pisma", "evanđelj", "evandelj", "poslanica",
  "starozavjetn", "novozavjetn", "citati", "tumačenj", "tumacenj",
  # Learning
  "znanje", "obrazov", "predavanj", "seminar", "tečaj", "tecaj",
  "knjiga", "knjige", "čitanj", "citanj", "učenj", "ucenj"
)

# CATEGORY ASSIGNMENT FUNCTION WITH WEIGHTED SCORING
assign_category <- function(words, topic_num) {
  words_str <- tolower(paste(words, collapse = " "))
  
  # Count matches for each category
  scores <- c(
    "Liturgical/Sacramental" = sum(sapply(liturgical_keywords, function(k) grepl(k, words_str))),
    "Devotional" = sum(sapply(devotional_keywords, function(k) grepl(k, words_str))),
    "Institutional" = sum(sapply(institutional_keywords, function(k) grepl(k, words_str))),
    "Social/Ethical" = sum(sapply(social_keywords, function(k) grepl(k, words_str))),
    "Political" = sum(sapply(political_keywords, function(k) grepl(k, words_str))),
    "Youth/Community" = sum(sapply(youth_keywords, function(k) grepl(k, words_str))),
    "Educational" = sum(sapply(educational_keywords, function(k) grepl(k, words_str)))
  )
  
  # If no matches, return Other
  if (max(scores) == 0) return("Other")
  
  # If tie, prefer more specific categories
  max_score <- max(scores)
  winners <- names(scores[scores == max_score])
  
  # Priority order for ties
  priority <- c("Liturgical/Sacramental", "Institutional", "Political", 
                "Social/Ethical", "Devotional", "Youth/Community", "Educational")
  
  for (p in priority) {
    if (p %in% winners) return(p)
  }
  
  return(winners[1])
}

# Apply classification
for (i in 1:K) {
  topic_taxonomy$Category[i] <- assign_category(topic_words_list[[i]], i)
  # Create more informative label using top 3 FREX words
  top_frex <- topic_words$frex[i, 1:3]
  topic_taxonomy$Label[i] <- paste(top_frex, collapse = ", ")
}

# Display results
topic_taxonomy %>%
  arrange(Category, Topic) %>%
  left_join(topic_prev_df %>% select(Topic, Prevalence) %>% 
              mutate(Topic = as.numeric(as.character(Topic))), by = "Topic") %>%
  mutate(Prevalence = sprintf("%.2f%%", Prevalence)) %>%
  kable(col.names = c("Topic", "Category", "Top FREX Words", "Prevalence")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  scroll_box(height = "500px")
Topic Category Top FREX Words Prevalence
3 Devotional međugorje, međugorju, atma 2.74%
6 Devotional amen, biblija, evanđelje 8.04%
20 Educational str, galović, tehnike 1.50%
5 Institutional franjevaca, zaređen, provincijal 3.26%
18 Institutional vatikan, papa, xiv 5.22%
25 Institutional sisačko, krapinsko, županija 3.37%
31 Institutional radoš, biskup, košić 3.12%
32 Institutional pravoslavne, koncila, stepinac 2.92%
34 Institutional ocijeni, kutleša, bozanić 3.38%
7 Liturgical/Sacramental kbr, ulicom, promet 2.41%
21 Liturgical/Sacramental laudato, svetištu, svetište 7.86%
35 Liturgical/Sacramental uskrsa, uskrs, božić 2.76%
2 Other film, ufc, section 1.99%
23 Other kolačići, spremiti, neophodni 1.23%
33 Other najniža, plaža, staza 2.25%
1 Political koncert, glazbe, koncertu 3.25%
4 Political razred, učenici, mentorica 2.33%
8 Political rusija, ruske, zelenski 2.65%
9 Political zbirke, slikar, dvorac 2.63%
10 Political vukovar, vukovara, škabrnje 1.71%
12 Political plenković, hdz, milanović 2.37%
16 Political pokolj, jasenovac, logora 1.44%
17 Political srpskog, ndh, hrvatima 2.39%
24 Political poslovanja, potrošača, kwh 2.30%
27 Political pretplatite, epizode, zvonce 2.94%
29 Political camino, dinamo, varaždin 2.25%
30 Political brigade, domovinske, bojne 2.56%
13 Social/Ethical virus, virusa, cjepiva 2.10%
14 Social/Ethical princ, charles, princeza 1.69%
15 Social/Ethical mama, šta, sjećam 3.84%
19 Social/Ethical zadar, zadru, zadra 1.39%
22 Social/Ethical djeteta, sestara, ccaron 2.70%
26 Social/Ethical mirovine, rimac, polovica 1.46%
28 Social/Ethical energija, sposobnost, osjećate 3.09%
11 Youth/Community đakovačko, osječke, pastoral 2.87%

3.2 Category Distribution

Show code
category_prevalence <- topic_taxonomy %>%
  left_join(topic_prev_df %>% 
              mutate(Topic = as.numeric(as.character(Topic))), by = "Topic") %>%
  group_by(Category) %>%
  summarise(
    Topics = n(),
    Total_Prevalence = sum(Prevalence, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(desc(Total_Prevalence))

ggplot(category_prevalence, aes(x = reorder(Category, Total_Prevalence), 
                                 y = Total_Prevalence, fill = Category)) +
  geom_col(width = 0.7) +
  geom_text(aes(label = sprintf("%.1f%%\n(%d topics)", Total_Prevalence, Topics)),
            hjust = -0.1, size = 3.5) +
  coord_flip() +
  scale_fill_brewer(palette = "Set2") +
  scale_y_continuous(limits = c(0, max(category_prevalence$Total_Prevalence) * 1.25),
                     labels = function(x) paste0(x, "%")) +
  labs(
    title = "Thematic Category Distribution",
    subtitle = "Aggregated prevalence by topic category",
    x = NULL,
    y = "Total Prevalence (%)"
  ) +
  theme(legend.position = "none")

4 Analysis 2.3: Topic by Actor Mapping

Examine how different actor types specialize in particular topics.

4.1 Understanding Actor-Topic Relationships

Different actors in the Catholic digital space have distinct communication priorities. Official institutions may focus on announcements and governance, while charismatic communities emphasize devotional content. This analysis reveals these specialization patterns.

Key concepts:

  • Topic proportion by actor: The average topic distribution for documents from each actor type
  • Specialization (Lift): How much more (or less) likely an actor type is to discuss a topic compared to the corpus average

Lift interpretation:

  • Lift = 1.0: Actor discusses this topic at the same rate as the overall corpus
  • Lift > 1.0: Actor specializes in this topic (e.g., Lift = 2.0 means twice as likely)
  • Lift < 1.0: Actor underrepresents this topic compared to average
Show code
doc_topics <- stm_model$theta
colnames(doc_topics) <- paste0("Topic_", 1:K)

doc_meta <- stm_dfm$meta
doc_meta <- cbind(doc_meta, doc_topics)

actor_topic_means <- doc_meta %>%
  group_by(actor_type) %>%
  summarise(across(starts_with("Topic_"), mean, na.rm = TRUE), .groups = "drop")

actor_topic_long <- actor_topic_means %>%
  pivot_longer(cols = starts_with("Topic_"),
               names_to = "Topic",
               values_to = "Proportion") %>%
  mutate(Topic_Num = as.numeric(gsub("Topic_", "", Topic)))

4.2 Topic Proportions by Actor Type Heatmap

This heatmap visualizes the relationship between actor types (columns) and topics (rows). Darker colors indicate higher proportions of that topic in an actor types content.

How to read the heatmap:

  • Each cell shows what percentage of an actor types content belongs to a topic
  • Rows are clustered by similarity, so related topics appear near each other
  • Columns are also clustered, revealing which actor types have similar thematic profiles
  • Look for dark cells to identify specializations; look for light/missing cells to identify topics an actor avoids
Show code
actor_topic_matrix <- actor_topic_means %>%
  column_to_rownames("actor_type") %>%
  as.matrix()

colnames(actor_topic_matrix) <- 1:K

topic_annotations <- topic_taxonomy %>%
  select(Topic, Category) %>%
  mutate(Topic = as.character(Topic)) %>%
  column_to_rownames("Topic")

pheatmap(
  t(actor_topic_matrix) * 100,
  cluster_rows = TRUE,
  cluster_cols = TRUE,
  color = colorRampPalette(c("white", "#2c5f7c", "#1a3c5a"))(100),
  main = "Topic Proportions by Actor Type (%)",
  fontsize = 10,
  fontsize_row = 8,
  fontsize_col = 9,
  angle_col = 45,
  display_numbers = FALSE,
  border_color = "grey90"
)

4.3 Actor Specialization Index

Calculate how specialized each actor type is compared to corpus average.

The Lift metric provides a precise measure of specialization. For each actor topic pair, we calculate:

\[\text{Lift} = \frac{\text{Actor's topic proportion}}{\text{Corpus average topic proportion}}\]

This table shows the top 3 topics where each actor type most strongly overindexes. A high lift value (e.g., 3.0x) indicates the actor discusses this topic three times more than average, suggesting it is central to their communication strategy.

Show code
corpus_avg <- colMeans(doc_topics)

specialization_df <- actor_topic_long %>%
  left_join(tibble(Topic_Num = 1:K, Corpus_Avg = corpus_avg), by = "Topic_Num") %>%
  mutate(
    Lift = Proportion / Corpus_Avg,
    Log_Lift = log2(Lift)
  ) %>%
  left_join(topic_taxonomy %>% select(Topic, Category, Label) %>%
              rename(Topic_Num = Topic), by = "Topic_Num")

top_specializations <- specialization_df %>%
  filter(is.finite(Log_Lift)) %>%
  group_by(actor_type) %>%
  slice_max(Log_Lift, n = 3) %>%
  ungroup()

top_specializations %>%
  select(actor_type, Label, Category, Lift) %>%
  mutate(Lift = sprintf("%.2fx", Lift)) %>%
  arrange(actor_type) %>%
  kable(col.names = c("Actor Type", "Topic", "Category", "Lift vs Corpus")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  scroll_box(height = "500px")
Actor Type Topic Category Lift vs Corpus
Academic ocijeni, kutleša, bozanić Institutional 8.54x
Academic đakovačko, osječke, pastoral Youth/Community 8.13x
Academic str, galović, tehnike Educational 5.40x
Charismatic Communities međugorje, međugorju, atma Devotional 22.33x
Charismatic Communities djeteta, sestara, ccaron Social/Ethical 2.57x
Charismatic Communities mama, šta, sjećam Social/Ethical 2.20x
Diocesan đakovačko, osječke, pastoral Youth/Community 7.89x
Diocesan radoš, biskup, košić Institutional 2.77x
Diocesan laudato, svetištu, svetište Liturgical/Sacramental 2.75x
Independent Media laudato, svetištu, svetište Liturgical/Sacramental 2.92x
Independent Media djeteta, sestara, ccaron Social/Ethical 2.14x
Independent Media pravoslavne, koncila, stepinac Institutional 2.11x
Institutional Official đakovačko, osječke, pastoral Youth/Community 3.25x
Institutional Official radoš, biskup, košić Institutional 2.75x
Institutional Official ocijeni, kutleša, bozanić Institutional 2.55x
Lay Influencers pretplatite, epizode, zvonce Political 7.17x
Lay Influencers amen, biblija, evanđelje Devotional 6.07x
Lay Influencers međugorje, međugorju, atma Devotional 1.81x
Other film, ufc, section Other 1.29x
Other pokolj, jasenovac, logora Political 1.28x
Other najniža, plaža, staza Other 1.28x
Religious Orders razred, učenici, mentorica Political 2.12x
Religious Orders sisačko, krapinsko, županija Institutional 1.95x
Religious Orders vukovar, vukovara, škabrnje Political 1.81x
Youth Organizations đakovačko, osječke, pastoral Youth/Community 19.42x
Youth Organizations camino, dinamo, varaždin Political 17.42x
Youth Organizations ocijeni, kutleša, bozanić Institutional 0.73x

4.4 Actor Type Topic Profiles

Show code
category_by_actor <- specialization_df %>%
  group_by(actor_type, Category) %>%
  summarise(Category_Proportion = sum(Proportion), .groups = "drop")

ggplot(category_by_actor, aes(x = Category, y = Category_Proportion, fill = Category)) +
  geom_col(width = 0.7) +
  facet_wrap(~actor_type, scales = "free_y", ncol = 3) +
  scale_fill_brewer(palette = "Set2") +
  scale_y_continuous(labels = function(x) paste0(round(x * 100, 1), "%")) +
  labs(
    title = "Thematic Category Profiles by Actor Type",
    subtitle = "What each actor type talks about",
    x = NULL,
    y = "Proportion of Content"
  ) +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1, size = 8),
    legend.position = "none",
    strip.text = element_text(face = "bold")
  )

5 Analysis 2.4: Topic by Platform Mapping

How do topics vary across digital platforms?

Different platforms have distinct affordances that shape communication: web allows long form content, Facebook enables community discussion, Instagram favors visual storytelling, and YouTube supports video content. These technical characteristics influence what topics appear where.

Expected patterns:

  • Web: More institutional, news oriented, and educational content (supports detailed text)
  • Social media (Facebook, Instagram): More devotional and community content (personal, shareable)
  • YouTube: Mix of educational and devotional (video catechesis, prayer channels)
  • Comments/Forums: More reactive content, discussion of controversial topics
Show code
platform_topic_means <- doc_meta %>%
  group_by(platform) %>%
  summarise(across(starts_with("Topic_"), mean, na.rm = TRUE), .groups = "drop")

platform_topic_long <- platform_topic_means %>%
  pivot_longer(cols = starts_with("Topic_"),
               names_to = "Topic",
               values_to = "Proportion") %>%
  mutate(Topic_Num = as.numeric(gsub("Topic_", "", Topic))) %>%
  left_join(topic_taxonomy %>% select(Topic, Category, Label) %>%
              rename(Topic_Num = Topic), by = "Topic_Num")

5.1 Platform Topic Heatmap

This heatmap uses a warm color palette (white to orange to red) to distinguish it from the actor heatmap. The same interpretation applies: darker colors indicate higher topic prevalence on that platform.

Show code
platform_topic_matrix <- platform_topic_means %>%
  column_to_rownames("platform") %>%
  as.matrix()

colnames(platform_topic_matrix) <- 1:K

pheatmap(
  t(platform_topic_matrix) * 100,
  cluster_rows = TRUE,
  cluster_cols = TRUE,
  color = colorRampPalette(c("white", "#e07b39", "#c44536"))(100),
  main = "Topic Proportions by Platform (%)",
  fontsize = 10,
  fontsize_row = 8,
  fontsize_col = 10,
  angle_col = 45,
  display_numbers = FALSE,
  border_color = "grey90"
)

5.2 Platform Category Profiles

Show code
category_by_platform <- platform_topic_long %>%
  group_by(platform, Category) %>%
  summarise(Category_Proportion = sum(Proportion), .groups = "drop")

ggplot(category_by_platform, aes(x = reorder(platform, Category_Proportion), 
                                  y = Category_Proportion, fill = Category)) +
  geom_col(position = "fill", width = 0.8) +
  coord_flip() +
  scale_fill_brewer(palette = "Set2") +
  scale_y_continuous(labels = percent) +
  labs(
    title = "Thematic Composition by Platform",
    subtitle = "Relative distribution of topic categories",
    x = NULL,
    y = "Proportion",
    fill = "Category"
  ) +
  theme(legend.position = "right")

5.3 Platform Specialization

Show code
platform_spec <- platform_topic_long %>%
  left_join(tibble(Topic_Num = 1:K, Corpus_Avg = corpus_avg), by = "Topic_Num") %>%
  mutate(Lift = Proportion / Corpus_Avg) %>%
  group_by(platform) %>%
  slice_max(Lift, n = 5) %>%
  ungroup()

platform_spec %>%
  select(platform, Label, Category, Lift) %>%
  mutate(Lift = sprintf("%.2fx", Lift)) %>%
  arrange(platform, desc(as.numeric(gsub("x", "", Lift)))) %>%
  kable(col.names = c("Platform", "Topic", "Category", "Lift")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE) %>%
  scroll_box(height = "400px")
Platform Topic Category Lift
comment srpskog, ndh, hrvatima Political 9.74x
comment virus, virusa, cjepiva Social/Ethical 4.50x
comment pravoslavne, koncila, stepinac Institutional 4.21x
comment mama, šta, sjećam Social/Ethical 2.61x
comment vatikan, papa, xiv Institutional 1.44x
facebook vatikan, papa, xiv Institutional 2.27x
facebook uskrsa, uskrs, božić Liturgical/Sacramental 1.89x
facebook međugorje, međugorju, atma Devotional 1.68x
facebook amen, biblija, evanđelje Devotional 1.58x
facebook laudato, svetištu, svetište Liturgical/Sacramental 1.51x
forum virus, virusa, cjepiva Social/Ethical 10.97x
forum mirovine, rimac, polovica Social/Ethical 4.76x
forum mama, šta, sjećam Social/Ethical 4.01x
forum zbirke, slikar, dvorac Political 2.21x
forum pravoslavne, koncila, stepinac Institutional 1.96x
instagram srpskog, ndh, hrvatima Political 4.25x
instagram pretplatite, epizode, zvonce Political 3.18x
instagram energija, sposobnost, osjećate Social/Ethical 2.82x
instagram film, ufc, section Other 2.62x
instagram amen, biblija, evanđelje Devotional 2.23x
reddit virus, virusa, cjepiva Social/Ethical 9.41x
reddit mirovine, rimac, polovica Social/Ethical 4.53x
reddit mama, šta, sjećam Social/Ethical 4.01x
reddit srpskog, ndh, hrvatima Political 3.27x
reddit pravoslavne, koncila, stepinac Institutional 2.78x
twitter srpskog, ndh, hrvatima Political 7.48x
twitter plenković, hdz, milanović Political 3.62x
twitter pravoslavne, koncila, stepinac Institutional 2.69x
twitter ocijeni, kutleša, bozanić Institutional 2.07x
twitter virus, virusa, cjepiva Social/Ethical 2.04x
web rusija, ruske, zelenski Political 1.29x
web poslovanja, potrošača, kwh Political 1.27x
web brigade, domovinske, bojne Political 1.26x
web sisačko, krapinsko, županija Institutional 1.23x
web razred, učenici, mentorica Political 1.22x
youtube pretplatite, epizode, zvonce Political 6.86x
youtube međugorje, međugorju, atma Devotional 3.74x
youtube amen, biblija, evanđelje Devotional 3.27x
youtube energija, sposobnost, osjećate Social/Ethical 1.87x
youtube laudato, svetištu, svetište Liturgical/Sacramental 1.68x

6 Analysis 2.5: Topic Engagement Differential

Which topics generate more engagement?

This analysis addresses a key hypothesis: that certain types of content (particularly political and controversial topics) generate disproportionately high engagement compared to their prevalence. This pattern, common in digital media, reflects the attention economy dynamics where provocative content outperforms routine communication.

Key metrics:

  • Weighted Mean Engagement: Average interactions per document, weighted by topic proportion. A document that is 80% about Topic X contributes more to Topic Xs engagement score than one that is 10% Topic X.
  • Total Engagement: Sum of all interactions attributed to a topic (useful for absolute comparison)
  • Engagement Efficiency: Topics above the trend line in the scatterplot punch above their weight, generating more engagement than their prevalence would predict

Interpretation:

  • Topics with high engagement but low prevalence represent niche but highly resonant content
  • Topics below the trend line may indicate routine, lower engagement content (e.g., announcements, schedules)
Show code
doc_engagement <- dta_sample[, .(doc_id, INTERACTIONS, REACH)]

# Create row indices for joining
doc_meta_with_id <- doc_meta |>
  mutate(row_id = row_number())

doc_engagement_with_id <- doc_engagement |>
  mutate(row_id = row_number())

# Join by row_id since both datasets have same order
doc_meta_eng <- doc_meta_with_id |>
  left_join(doc_engagement_with_id, by = "row_id", suffix = c("", "_y"))

# Now proceed with topic engagement calculation
topic_engagement <- lapply(1:K, function(t) {
  topic_col <- paste0("Topic_", t)
  doc_meta_eng |>
    mutate(weight = .data[[topic_col]]) |>
    summarise(
      Topic = t,
      Weighted_Mean_Engagement = weighted.mean(INTERACTIONS, weight, na.rm = TRUE),
      Mean_Engagement = mean(INTERACTIONS[.data[[topic_col]] > 0.1], na.rm = TRUE),
      Total_Engagement = sum(INTERACTIONS * weight, na.rm = TRUE),
      Document_Count = sum(.data[[topic_col]] > 0.1)
    )
}) |> bind_rows()

topic_engagement <- topic_engagement |>
  left_join(topic_taxonomy, by = "Topic") |>
  left_join(topic_prev_df |> 
              mutate(Topic = as.numeric(as.character(Topic))), by = "Topic")

6.1 Topic Engagement Ranking

Show code
ggplot(topic_engagement, aes(x = reorder(factor(Topic), Weighted_Mean_Engagement),
                              y = Weighted_Mean_Engagement, fill = Category)) +
  geom_col(width = 0.7) +
  geom_text(aes(label = round(Weighted_Mean_Engagement, 0)), 
            hjust = -0.1, size = 3) +
  coord_flip() +
  scale_fill_brewer(palette = "Set2") +
  scale_y_continuous(limits = c(0, max(topic_engagement$Weighted_Mean_Engagement, na.rm = TRUE) * 1.15)) +
  labs(
    title = "Average Engagement by Topic",
    subtitle = "Weighted mean interactions per document",
    x = "Topic",
    y = "Weighted Mean Engagement",
    fill = "Category"
  )

6.2 Volume vs Engagement Comparison

This scatterplot is one of the most important visualizations in the analysis. Each point represents a topic, positioned by its prevalence (x axis) and average engagement (y axis).

How to interpret:

  • Trend line (dashed): Shows the expected relationship between prevalence and engagement
  • Points above the line: Topics that generate more engagement than expected given their prevalence (engagement overperformers)
  • Points below the line: Topics that underperform in engagement relative to their volume
  • Point size: Indicates total engagement volume

This visualization directly tests the hypothesis that certain content types (often political or controversial) punch above their weight in the attention economy.

Show code
topic_engagement <- topic_engagement %>%
  mutate(
    Prevalence_Rank = rank(-Prevalence),
    Engagement_Rank = rank(-Weighted_Mean_Engagement),
    Engagement_Efficiency = Engagement_Rank - Prevalence_Rank
  )

ggplot(topic_engagement, aes(x = Prevalence, y = Weighted_Mean_Engagement)) +
  geom_point(aes(color = Category, size = Total_Engagement), alpha = 0.7) +
  geom_text_repel(aes(label = Topic), size = 3, max.overlaps = 15) +
  geom_smooth(method = "lm", se = TRUE, linetype = "dashed", color = "gray50") +
  scale_color_brewer(palette = "Set2") +
  scale_size_continuous(range = c(3, 12), labels = comma) +
  labs(
    title = "Topic Prevalence vs Engagement",
    subtitle = "Points above the trend line are engagement overperformers",
    x = "Prevalence (%)",
    y = "Weighted Mean Engagement",
    color = "Category",
    size = "Total Engagement"
  )

6.3 Engagement Efficiency by Category

Show code
category_engagement <- topic_engagement %>%
  group_by(Category) %>%
  summarise(
    Mean_Engagement = mean(Weighted_Mean_Engagement, na.rm = TRUE),
    Total_Prevalence = sum(Prevalence),
    Engagement_per_Prevalence = Mean_Engagement / Total_Prevalence,
    .groups = "drop"
  ) %>%
  arrange(desc(Engagement_per_Prevalence))

ggplot(category_engagement, aes(x = Total_Prevalence, y = Mean_Engagement)) +
  geom_point(aes(color = Category), size = 8) +
  geom_text_repel(aes(label = Category), size = 4) +
  scale_color_brewer(palette = "Set2") +
  labs(
    title = "Category Prevalence vs Engagement",
    subtitle = "Which thematic categories punch above their weight?",
    x = "Total Prevalence (%)",
    y = "Mean Engagement per Topic"
  ) +
  theme(legend.position = "none")

Key Finding on Engagement Differential

Topics in the Political and Social/Ethical categories often generate disproportionately high engagement relative to their prevalence, supporting the hypothesis that controversial content attracts more attention.

8 Summary and Key Findings

Show code
# Debug: show what we have
cat("category_prevalence columns:", paste(names(category_prevalence), collapse = ", "), "\n")
category_prevalence columns: Category, Topics, Total_Prevalence 
Show code
cat("category_engagement columns:", paste(names(category_engagement), collapse = ", "), "\n")
category_engagement columns: Category, Mean_Engagement, Total_Prevalence, Engagement_per_Prevalence 
Show code
# Join category data
category_summary <- category_prevalence %>%
  left_join(category_engagement, by = "Category")

cat("category_summary columns:", paste(names(category_summary), collapse = ", "), "\n")
category_summary columns: Category, Topics, Total_Prevalence.x, Mean_Engagement, Total_Prevalence.y, Engagement_per_Prevalence 
Show code
cat("category_summary rows:", nrow(category_summary), "\n")
category_summary rows: 8 
Show code
# Get top category by prevalence (handle if column missing)
if ("Total_Prevalence" %in% names(category_summary)) {
  top_category <- category_summary %>% 
    filter(!is.na(Total_Prevalence)) %>%
    slice_max(Total_Prevalence, n = 1)
  top_cat_name <- top_category$Category[1]
  top_cat_prev <- round(top_category$Total_Prevalence[1], 1)
} else {
  top_cat_name <- "Unknown"
  top_cat_prev <- 0
}

# Get top category by engagement  
if ("Mean_Engagement" %in% names(category_summary)) {
  top_engagement_cat <- category_summary %>% 
    filter(!is.na(Mean_Engagement)) %>%
    slice_max(Mean_Engagement, n = 1)
  top_eng_cat_name <- top_engagement_cat$Category[1]
} else {
  top_eng_cat_name <- "Unknown"
}

cat("Top category:", top_cat_name, "with", top_cat_prev, "% prevalence\n")
Top category: Unknown with 0 % prevalence
Show code
cat("Top engagement category:", top_eng_cat_name, "\n")
Top engagement category: Devotional 

8.1 Thematic Structure Findings

  1. Dominant themes: The corpus is dominated by Unknown content (0% prevalence)

  2. Engagement patterns: Devotional content generates the highest average engagement despite not being the most prevalent

  3. Actor specialization: Clear patterns emerge in what different actors talk about

  4. Platform differences: Web platforms show more institutional/news content while social platforms show more devotional and community content

8.2 Topic Model Summary Table

Show code
topic_engagement %>%
  select(Topic, Label, Category, Prevalence, Weighted_Mean_Engagement) %>%
  arrange(desc(Prevalence)) %>%
  head(15) %>%
  mutate(
    Prevalence = sprintf("%.1f%%", Prevalence),
    Weighted_Mean_Engagement = round(Weighted_Mean_Engagement, 0)
  ) %>%
  kable(col.names = c("Topic", "Label", "Category", "Prevalence", "Mean Engagement")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Topic Label Category Prevalence Mean Engagement
6 amen, biblija, evanđelje Devotional 8.0% 203
21 laudato, svetištu, svetište Liturgical/Sacramental 7.9% 70
18 vatikan, papa, xiv Institutional 5.2% 59
15 mama, šta, sjećam Social/Ethical 3.8% 189
34 ocijeni, kutleša, bozanić Institutional 3.4% 62
25 sisačko, krapinsko, županija Institutional 3.4% 822
5 franjevaca, zaređen, provincijal Institutional 3.3% 122
1 koncert, glazbe, koncertu Political 3.3% 60
31 radoš, biskup, košić Institutional 3.1% 61
28 energija, sposobnost, osjećate Social/Ethical 3.1% 93
27 pretplatite, epizode, zvonce Political 2.9% 156
32 pravoslavne, koncila, stepinac Institutional 2.9% 92
11 đakovačko, osječke, pastoral Youth/Community 2.9% 65
35 uskrsa, uskrs, božić Liturgical/Sacramental 2.8% 59
3 međugorje, međugorju, atma Devotional 2.7% 247

8.3 Hypotheses Testing Summary

Hypothesis Finding
H7: Devotional/liturgical content dominates volume Examine category prevalence results above
H8: Political/social content generates disproportionate engagement Compare engagement efficiency by category
H9: Individual priests concentrate on social/political topics See actor specialization patterns

9 Alternative Approaches and Future Directions

9.1 Limitations of STM/LDA

The Structural Topic Model used in this analysis is a bag-of-words approach that:

  • Ignores word order: The phrase “Pope Francis visits Croatia” is treated the same as “Croatia visits Pope Francis”
  • Assumes topics are independent: Real themes often overlap and interact
  • Requires manual interpretation: Topic labels are assigned by humans examining word lists
  • Sensitive to preprocessing: Different stopword lists and tokenization produce different results
  • Fixed number of topics: K must be chosen in advance; different K values yield different topic structures

9.2 Modern Alternatives

Several newer approaches could improve thematic analysis:

9.2.1 1. BERTopic (Transformer-based Topic Modeling)

BERTopic uses pretrained language models to create semantically meaningful embeddings before clustering.

Advantages: - Captures semantic similarity beyond word co-occurrence - Handles synonyms and related concepts automatically - Produces more coherent, interpretable topics - Does not require extensive stopword lists

Implementation in R/Python:

# Python example (requires bertopic package)
from bertopic import BERTopic
from sentence_transformers import SentenceTransformer

# Use multilingual model for Croatian
embedding_model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
topic_model = BERTopic(embedding_model=embedding_model, language="multilingual")
topics, probs = topic_model.fit_transform(documents)

9.2.2 2. Multilingual Language Models

For Croatian text, several pretrained models are available:

Model Description Use case
mBERT (multilingual BERT) Trained on 104 languages including Croatian General embeddings
XLM-RoBERTa Strong multilingual performance Semantic similarity
CroSloEngual BERT Specialized for Croatian, Slovenian, English Best for Croatian
paraphrase-multilingual-MiniLM Efficient, good for clustering BERTopic embeddings

9.2.3 3. Zero-shot Classification

Instead of discovering topics, assign documents to predefined categories using language models:

from transformers import pipeline

classifier = pipeline("zero-shot-classification", 
                      model="joeddav/xlm-roberta-large-xnli")

candidate_labels = ["liturgija", "politika", "obitelj", "molitva", "Vatikan"]
result = classifier(text, candidate_labels, multi_label=True)

9.2.4 4. Named Entity Recognition for Croatian

Extract structured information (people, organizations, places) using: - classla (for Slovenian and Croatian NLP) - stanza with Croatian models - spaCy with custom Croatian models

9.2.5 5. Semantic Search and Clustering

Modern approach workflow: 1. Embed all documents using multilingual transformers 2. Reduce dimensions with UMAP 3. Cluster with HDBSCAN 4. Extract topic labels using c-TF-IDF or LLM summarization

9.4 Tools and Resources

Tool Language Purpose
BERTopic Python Modern topic modeling
sentence-transformers Python Text embeddings
classla Python Croatian NLP pipeline
text2vec R Traditional embeddings
quanteda.textmodels R Enhanced topic models
keyATM R Keyword-guided topic models

10 Appendix: Model Diagnostics

Show code
cat("Topic Model Summary:\n")
Topic Model Summary:
Show code
cat("Number of topics:", K, "\n")
Number of topics: 35 
Show code
cat("Documents:", nrow(doc_topics), "\n")
Documents: 6046 
Show code
cat("Vocabulary size:", length(stm_model$vocab), "\n")
Vocabulary size: 28829 
Show code
sessionInfo()
R version 4.5.2 (2025-10-31 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 22631)

Matrix products: default
  LAPACK version 3.12.1

locale:
[1] LC_COLLATE=Croatian_Croatia.utf8  LC_CTYPE=Croatian_Croatia.utf8   
[3] LC_MONETARY=Croatian_Croatia.utf8 LC_NUMERIC=C                     
[5] LC_TIME=Croatian_Croatia.utf8    

time zone: Europe/Zagreb
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] RColorBrewer_1.1-3        pheatmap_1.0.13          
 [3] viridis_0.6.5             viridisLite_0.4.2        
 [5] ggrepel_0.9.6             tidytext_0.4.3           
 [7] stm_1.3.8                 quanteda.textplots_0.96.1
 [9] quanteda.textstats_0.97.2 quanteda_4.3.1           
[11] text2vec_0.6.6            kableExtra_1.4.0         
[13] knitr_1.50                scales_1.4.0             
[15] data.table_1.17.8         lubridate_1.9.4          
[17] forcats_1.0.1             stringr_1.6.0            
[19] dplyr_1.1.4               purrr_1.2.0              
[21] readr_2.1.6               tidyr_1.3.1              
[23] tibble_3.3.0              ggplot2_4.0.1            
[25] tidyverse_2.0.0          

loaded via a namespace (and not attached):
 [1] fastmatch_1.1-6     gtable_0.3.6        xfun_0.54          
 [4] htmlwidgets_1.6.4   lattice_0.22-7      tzdb_0.5.0         
 [7] vctrs_0.6.5         tools_4.5.2         generics_0.1.4     
[10] janeaustenr_1.0.0   tokenizers_0.3.0    pkgconfig_2.0.3    
[13] Matrix_1.7-4        S7_0.2.1            lifecycle_1.0.4    
[16] compiler_4.5.2      farver_2.1.2        textshaping_1.0.4  
[19] RhpcBLASctl_0.23-42 codetools_0.2-20    SnowballC_0.7.1    
[22] htmltools_0.5.8.1   yaml_2.3.11         pillar_1.11.1      
[25] crayon_1.5.3        nlme_3.1-168        rsparse_0.5.3      
[28] stopwords_2.3       wordcloud_2.6       tidyselect_1.2.1   
[31] digest_0.6.39       stringi_1.8.7       splines_4.5.2      
[34] labeling_0.4.3      fastmap_1.2.0       grid_4.5.2         
[37] cli_3.6.5           magrittr_2.0.4      dichromat_2.0-0.1  
[40] withr_3.0.2         float_0.3-3         timechange_0.3.0   
[43] rmarkdown_2.30      matrixStats_1.5.0   gridExtra_2.3      
[46] mlapi_0.1.1         hms_1.1.4           evaluate_1.0.5     
[49] mgcv_1.9-3          rlang_1.1.6         Rcpp_1.1.0         
[52] nsyllable_1.0.1     glue_1.8.0          xml2_1.5.1         
[55] svglite_2.2.2       rstudioapi_0.17.1   jsonlite_2.0.0     
[58] lgr_0.5.0           R6_2.6.1            systemfonts_1.3.1